import "./style.css"

declare global {
    interface Array<T> {
        includes(v: T): boolean;
        remove(v: T): boolean;
    }
    interface Window {
        plc: any;
    }
}

if (!Array.prototype.remove) {
    Array.prototype.remove = function (item: any) {
        var index = this.indexOf(item);
        if (index >= 0) {
            this.splice(index, 1);
        }
        return (index >= 0);
    }
}

type Simpletype = string | number | Date | boolean;
type DT<T extends Simpletype> = [string] | [string, T] | [string, T, number];
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type TagsDefine = {
    [k: string]: {
        addr?: string;
        tags: {
            [k: string]: DT<any>
        }
    }
};
type TagsProperty<T extends TagsDefine> = UnionToIntersection<T[keyof T]["tags"]>;
type TagNames<T extends TagsDefine> = keyof TagsProperty<T>;
type TagValues<T extends TagsDefine, K extends TagNames<T>> = Exclude<TagsProperty<T>[K][1 & keyof TagsProperty<T>[K]], undefined>;
// #region Tags define
type NumberFilter<T extends TagsDefine, M extends TagNames<T>> = TagValues<T, M> extends number ? M : never;
type Tags<T extends TagsDefine> = {
    [k in TagNames<T>]: TagValues<T, k>;
} &
    {
        [k in TagNames<T> as `${string & k}_Quality`]: number;
    } & {
        [k in TagNames<T> as `${string & k}_TimeStamp`]: number;
    } & {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_Precision`]: 0 | 1;
    } & {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit0`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit1`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit2`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit3`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit4`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit5`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit6`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit7`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit8`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit9`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit10`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit11`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit12`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit13`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit14`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit15`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit16`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit17`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit18`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit19`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit20`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit21`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit22`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit23`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit24`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit25`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit26`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit27`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit28`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit29`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit30`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit31`]: 0 | 1;
    } &
    {
        [k in TagNames<T> as `${string & NumberFilter<T, k>}_bit32`]: 0 | 1;
    };
// #endregion

type TagsTag<T extends TagsDefine> = {
    [k: string]: PLCTag<T>;
}

type Equipments<T extends TagsDefine> = {
    [p: string]: Equipment<T>;
}
type PLCMSG = {
    name: string;
    value: any;
    timeStamp: number;
    quality: number;
}[]

type TagChangeEventType<T extends TagsDefine> = (tag: Tags<T>) => boolean | (0 | 1);
interface PLCEvent<T> {
    sender?: T;
    type?: string;
    equipmentName?: string;
    equipmentAddr?: string;
}
interface TagChangeEventParam<T extends TagsDefine> {
    sender: PLCTag<T>;
    type: string;
    tagName: string;
    tagAddr?: string;
    devValue: any;
    tagValue: any;
}
interface PLCErrorEvent<T> extends PLCEvent<T> {
    code: number;
    msg: string;
}
interface PLCEventMap<T> {
    error: PLCErrorEvent<T>;
    open: PLCEvent<T>;
}

type PLCEventType<T extends TagsDefine> = keyof PLCEventMap<any> | TagNames<T> | TagNames<T>[] | TagChangeEventType<T>

type TFEvent<T extends TagsDefine, This, SCOPE> = {
    onTrue(this: unknown extends SCOPE ? This : SCOPE, e: TagChangeEventParam<T>): void;
    onTrueDelay?: number;
    onTrueInterval?: number;
    onFalse?(this: unknown extends SCOPE ? This : SCOPE, e: TagChangeEventParam<T>): void;
    onFalseDelay?: number;
    onFalseInterval?: number;
}
type FTEvent<T extends TagsDefine, This, SCOPE> = {
    onTrue?(this: unknown extends SCOPE ? This : SCOPE, e: TagChangeEventParam<T>): void;
    onTrueDelay?: number;
    onTrueInterval?: number;
    onFalse(this: unknown extends SCOPE ? This : SCOPE, e: TagChangeEventParam<T>): void;
    onFalseDelay?: number;
    onFalseInterval?: number;
}

type TagChangeState<T extends TagsDefine> = {
    prestatus: boolean;
    expression: (e: AppHandler<T>) => boolean;
    trueDelay: number;
    trueInterval: number;
    falseDelay: number;
    falseInterval: number;
    active: boolean;
    tags: Set<PLCTag<T>>;
}

interface Node {
    [k: string]: any,
}

type TagChangeEventSetting<T extends TagsDefine> = {
    handler: Function | (Partial<TFEvent<T, any, any>> & TagChangeState<T>),
    scope: any,
    once: boolean
}
class PLCTag<T extends TagsDefine>{
    private maddr?: string;
    private mname: string;
    private mvalue!: Simpletype;
    private mdevValue?: Simpletype;
    private mprecision?: number;
    private mquality?: number;
    private mtimeStamp?: Date;
    private mactive: boolean = false;
    private mevents: { [k: string]: TagChangeEventSetting<T>[] } = {};
    //node,   setter, attribute,oriText,active,tags
    private mhotPoints: HotPoint[] = [];
    private mplc: AppHandler<T>;
    private meqp?: Equipment<T>;
    constructor(plc: AppHandler<T>, tagname: string, value?: Simpletype, eqp?: Equipment<T>, addr?: string) {
        if (value !== undefined) this.mvalue = value;
        this.mname = tagname;
        this.mplc = plc;
        if (eqp) this.meqp = eqp;
        if (addr) this.maddr = addr;
    }
    private notify() {
        AppHandler.mutationsCollector.stop();
        this.mhotPoints.forEach(e => {
            const { node, attributeName1, attributeName2, oldValue, activate } = e;
            let newValue: Simpletype;
            if (!activate) return;

            if (oldValue.match(AppHandler.mutationsCollector["regFullProperty"])) {
                newValue = eval(oldValue.slice(2, -2));
            } else {
                newValue = oldValue.replace(AppHandler.mutationsCollector["regProperty"], e => {
                    const rtn = eval(e.slice(2, -2))
                    return rtn;
                })
            }
            if (attributeName2) {
                node[attributeName1][attributeName2] = newValue;
            } else {
                const oldValue = node[attributeName1]
                //清除组内其它的radio，保存只有一个选中值 
                if (node.type === "radio" && newValue) {
                    const event = new CustomEvent("change");
                    document.querySelectorAll(`input[name=${node.name}]`).forEach(e => {
                        if (e == node || (<HTMLInputElement>e).type !== 'radio') return;
                        (<HTMLInputElement>e).checked = false;
                        e.dispatchEvent(event);
                    })
                }
                if (oldValue == newValue) { return; }
                node[attributeName1] = newValue;
                if (attributeName1 == "checked" || attributeName1 == "value") {
                    const event = new CustomEvent("change");
                    node.dispatchEvent(event);
                }
            }
        })
        AppHandler.mutationsCollector.start();
    }
    public removeHotPoint(hotPoint: HotPoint) {
        this.mhotPoints.remove(hotPoint);
    }
    set value(v: Simpletype) {
        if (this.mprecision && typeof v == "number") {
            v = +v.toFixed(this.mprecision);
        }
        if (this.mvalue != v) {
            if (this.maddr && this.mactive) {
                this.meqp!["writeTag"](this.maddr, v);
            }
            this.mvalue = v;
            if (this.mactive) {
                this.fire("change");
            } else {
                console.log("Write Tag in unActive status. Tag:%s", this.mname);
            }
        }
    }
    get value(): Simpletype {
        return this.mvalue;
    }
    set active(v: boolean) {
        if (this.mactive == v) return;
        this.mactive = v;
        if (v) {
            if (this.mevents.change) {
                this.mevents.change.forEach(i => {
                    if (typeof i.handler !== "object") return;
                    let active = true;
                    for (let tag of i.handler.tags) {
                        if (!tag.mactive) active = false
                        break;
                    }
                    i.handler.active = active;
                })
            }
            this.mhotPoints.forEach(i => {
                let activate = true;
                for (let tag of i.notifier) {
                    if (!tag.mactive) activate = false
                }
                i.activate = activate;
            })
            this.fire("change");
        } else {
            if (this.mevents.change) {
                this.mevents.change.forEach(i => {
                    if (typeof i.handler !== "object") return;
                    i.handler.active = false;
                })
            }
            this.mhotPoints.forEach(i => {
                i.activate = false;
            })
        }
    }
    private on(type: string, handler: any, scope: any, once: boolean) {
        if (!this.mevents[type]) this.mevents[type] = [];
        if (typeof handler == "object") {
            handler.active = this.mactive && handler.active;
        }
        this.mevents[type].push({ handler, scope, once })
    }
    private fire(type: string, e?: TagChangeEventParam<T>) {
        if (!this.mactive) return;
        this.notify();
        const handlers = this.mevents[type];
        if (!handlers) return;
        e = e || {} as TagChangeEventParam<T>;
        e.type = type;
        e.sender = this;
        e.tagName = this.mname;
        e.tagValue = this.mvalue.valueOf();
        if (this.maddr) {
            e.tagAddr = this.maddr;
            e.devValue = this.mdevValue;
        }
        handlers.forEach((i, idx, array) => {
            if (i.once) array.splice(idx, 1);
            if (typeof i.handler == "function") {
                i.handler.call(i.scope, e);
                return;
            }
            if (typeof i.handler !== "object" || !i.handler.active) return;
            const handler = i.handler;
            const rtn = handler.expression(this.mplc);

            if (rtn == handler.prestatus) return;

            handler.prestatus = rtn;

            if (rtn && handler.onTrue) {
                if (handler.onTrueDelay) {
                    handler.trueDelay = setTimeout(() => {
                        if (handler.onTrueInterval) {
                            handler.trueInterval = setInterval(() => {
                                handler.onTrue!.call(i.scope, e!);
                            }, handler.onTrueInterval)
                        } else {
                            handler.onTrue!.call(i.scope, e!);
                        }
                    }, handler.onTrueDelay);
                } else if (handler.onTrueInterval) {
                    handler.trueInterval = setInterval(() => {
                        handler.onTrue!.call(i.scope, e!);
                    }, handler.onTrueInterval)
                } else {
                    handler.onTrue.call(i.scope, e!);
                }
            } else {
                if (handler.onTrue) {
                    if (handler.trueInterval) {
                        clearInterval(handler.trueInterval);
                        handler.trueInterval = 0;
                    }
                    if (handler.trueDelay) {
                        clearTimeout(handler.trueDelay);
                        handler.trueDelay = 0;
                    }
                }
            }

            if (!rtn && i.handler.onFalse) {
                if (handler.onFalseDelay) {
                    handler.falseDelay = setTimeout(() => {
                        if (handler.onFalseInterval) {
                            handler.falseInterval = setInterval(() => {
                                handler.onFalse!.call(i.scope, e!);
                            }, handler.onFalseInterval)
                        } else {
                            handler.onFalse!.call(i.scope, e!);
                        }
                    }, handler.onFalseDelay);
                } else if (handler.onFalseInterval) {
                    handler.falseInterval = setInterval(() => {
                        handler.onFalse!.call(i.scope, e!);
                    }, handler.onFalseInterval)
                } else {
                    handler.onFalse!.call(i.scope, e!);
                }
            } else {
                if (handler.onFalse) {
                    if (handler.falseInterval) {
                        clearInterval(handler.falseInterval);
                        handler.falseInterval = 0;
                    }
                    if (handler.falseDelay) {
                        clearTimeout(handler.falseDelay);
                        handler.falseDelay = 0;
                    }
                }
            }

        })
    }
}
class Equipment<T extends TagsDefine>{
    private mWebSocket!: WebSocket;
    private maddr: string;
    private mname: string;
    private mtags: TagsTag<T> = {};
    private mplc: AppHandler<T>;
    //public events: any[] = [];
    public status: "closed" | "connecting" | "opened" = 'closed'
    private doOpen() {
        this.status = "opened";
        if (this.mplc["mevents"]["open"]) {
            this.mplc["fire"]("open", { equipmentName: this.mname, equipmentAddr: this.maddr });
        } else {
            console.log(`PLC(${this.maddr}) started successfully.`)
        }
        for (let tag in this.mtags) {
            const quality = this.mtags[tag]["mquality"];
            if (quality && quality == 192) {
                this.mtags[tag].active = true;
                this.mtags[tag]["fire"]("change");
            }
        }
        let allopened = true;
        for (let i in this.mplc["mequipments"]) {
            if (this.mplc["mequipments"][i].status != "opened") {
                allopened = false;
                break;
            }
        }
        if (allopened) {
            document.body.style.visibility = "visible";
        }
    }
    private onWSOpen(_: Event) {
        let reqs: string[] = [];
        let cmd = {
            cmd: "regist",
            pt: reqs
        }
        let preonmsg = this.mWebSocket.onmessage;
        for (let k in this.mtags) {
            reqs.push(k);
        }
        if (reqs.length == 0) return;
        let timer = setTimeout(() => {
            this.mWebSocket.onmessage = preonmsg;
            let errorTags = reqs.join(",");
            reqs = [];
            if (this.mplc["mevents"]["error"]) {
                this.mplc["fire"]("error", { equipmentAddr: this.maddr, equipmentName: this.mname, code: 1000, msg: `Regist PLC(${this.maddr}) point timeout, abnormal PLC points:${errorTags}` });
            } else {
                console.log(`Regist PLC point timeout, abnormal PLC points:${errorTags}`);
            }
            this.doOpen();
        }, reqs.length * 30 + 500);

        this.mWebSocket.onmessage = e => {
            let o = this.decodeMsg(e.data);
            for (let i of o) {
                let idx = -1;
                idx = reqs.indexOf(i.name);
                if (idx >= 0) {
                    reqs.splice(idx, 1);
                    if (reqs.length == 0) {
                        clearTimeout(timer);
                        this.mWebSocket.onmessage = preonmsg;
                        this.doOpen();
                        break;
                    }
                }
            }
        }
        this.mWebSocket.send(JSON.stringify(cmd));
    }
    private onWSError(_: Event) {
        for (let tag in this.mtags) this.mtags[tag].active = false;
        const msg = `An exception occurred while connecting to the OPC server(${this.maddr}). The connection will be retried in 1 second.`
        if (this.mplc["mevents"]["error"]) {
            this.mplc["fire"]("error", { equipmentAddr: this.maddr, equipmentName: this.mname, code: 1001, msg: msg });
        } else {
            console.log(msg);
        }
        this.status = "connecting";
        document.body.style.visibility = "visible"
        this.mWebSocket.onclose = null;
        if (this.mWebSocket.readyState < 3) this.mWebSocket.close();
        setTimeout(() => {
            this.open();
        }, 1000);
    }
    private onWSClose(_: Event) {
        this.status = "closed";
        document.body.style.visibility = "visible"
        for (let tag in this.mtags) this.mtags[tag].active = false;
        const msg = `The connection to the OPC server(${this.maddr}) was disconnected and will be retried in 1 second.`
        if (this.mplc["mevents"]["error"]) {
            this.mplc["fire"]("error", { code: 1001, msg: msg });
        } else {
            console.log(msg);
        }
        setTimeout(() => {
            this.open();
        }, 1000);
    }
    private decodeMsg(msg: string) {
        const o: PLCMSG = JSON.parse(msg);
        o.forEach(e => {
            const tag = this.mtags[e.name];
            if (tag["mprecision"] && typeof e.value == "number") e.value = +e.value.toFixed(tag["mprecision"]);
            tag["mdevValue"] = e.value;
            if (this.status == "opened" && e.quality == 192)
                tag.active = true;
            else {
                tag.active = false;
            }
            if (tag["mvalue"] != e.value) {
                tag["mvalue"] = e.value
                tag["fire"]("change");
            }
            tag["mquality"] = e.quality;
            // tag["mtimeStamp"] = new Date((e.timeStamp - 25569) * 24 * 3600 * 1000);
            tag["mtimeStamp"] = new Date(e.timeStamp);
        });
        return o;
    }
    private registTags(tags: T[keyof T]["tags"]) {
        for (const k in tags) {
            const addr = tags[k][0]
            if (!addr) throw new Error(`The OPC access address is not specified explicitly,The tag name in error is: ${k}`);
            const tag = new PLCTag<T>(this.mplc, k, undefined, this, addr)
            this.mtags[addr] = tag;
            this.mplc.tags[k] = tag;
            this.mplc["defineProperty"](k);
            this.mplc["defineProperty"](k + "_Quality");
            this.mplc["defineProperty"](k + "_TimeStamp");
            if (typeof tags[k][1] == "number") {
                for (let i = 0; i < 32; i++) {
                    this.mplc["defineProperty"](k + "_bit" + i);
                }
            }
            if (tags[k][1] && typeof tags[k][1] == "number") {
                tag["mprecision"] = tags[k][1];
                this.mplc["defineProperty"](k + "_Precision");
            }
        }
    }
    private unload() {
        if (this.mWebSocket && this.mWebSocket.readyState < 3) {
            this.mWebSocket.onclose = null;
            this.mWebSocket.close();
            (<any>this.mWebSocket) = undefined;
        }
        window.removeEventListener("unload", this.bind_unload!);
        this.bind_unload = undefined;
    }
    private bind_unload: (() => void) | undefined;
    private open(): void {
        this.mWebSocket = new WebSocket(this.maddr);
        if (!this.bind_unload) {
            this.bind_unload = this.unload.bind(this);
            window.addEventListener("unload", this.bind_unload);
        }
        this.mWebSocket.onmessage = (e) => this.decodeMsg(e.data);
        this.mWebSocket.onopen = this.onWSOpen.bind(this);
        this.mWebSocket.onclose = this.onWSClose.bind(this);
        this.mWebSocket.onerror = this.onWSError.bind(this);
    }
    protected writeTag = (() => {
        let reqs: { [k: string]: any } = {};
        let timer: number | undefined;
        const me = this;
        return (addr: string, value: any) => {
            reqs[addr] = value;
            if (timer) return;
            timer = setTimeout(() => {
                const cmd = {
                    cmd: "write",
                    pt: reqs
                }
                me.mWebSocket.send(JSON.stringify(cmd));
                reqs = {};
                timer = undefined;
            }, 1);
        };
    })()
    constructor(plc: AppHandler<T>, tags: T[keyof T], name: string, autoOpen: boolean) {
        if (!tags.addr)
            throw new Error(`Unknown device address(${name}:${tags.addr}). Please specify the right device address.`);
        this.mplc = plc;
        this.maddr = tags.addr;
        this.mname = name;
        this.registTags(tags.tags);
        if (autoOpen) this.open();
    }
}

class AppHandler<T extends TagsDefine> {
    static mutationsCollector: MutationCollector;
    public tags: TagsTag<T> = {};
    private mevents: { [k: string]: any[] } = {};
    private mequipments: Equipments<T> = {};
    public open() {
        for (let key in this.mequipments) {
            this.mequipments[key]["open"]();
        }
    }
    private defineProperty(prop: string) {
        let mc;
        if (prop.endsWith("_Quality")) {
            const prop1 = prop.substring(0, prop.lastIndexOf("_"));
            Object.defineProperty(this, prop, {
                enumerable: true,
                configurable: false,
                get() {
                    return this.tags[prop1]["mquality"];
                }
            })
        } else if (prop.endsWith("_TimeStamp")) {
            const prop1 = prop.substring(0, prop.lastIndexOf("_"));
            Object.defineProperty(this, prop, {
                enumerable: true,
                configurable: false,
                get() {
                    return this.tags[prop1]["mtimeStamp"];
                }
            })
        } else if (prop.endsWith("_Precision")) {
            const prop1 = prop.substring(0, prop.lastIndexOf("_"));
            Object.defineProperty(this, prop, {
                enumerable: true,
                configurable: false,
                get() {
                    return this.tags[prop1]["mprecision"];
                }
            })
        } else if (mc = prop.match(/(.*?)_bit([0-9]{1,2}$)/)) {
            const prop1 = mc[1];
            const mask = 1 << (+mc[2]);
            Object.defineProperty(this, prop, {
                enumerable: true,
                configurable: false,
                get(): 1 | 0 {
                    return (this.tags[prop1].value & mask) == 0 ? 0 : 1;
                },
                set(v: 0 | 1) {
                    v > 0 ? this.tags[prop1].value |= mask : this.tags[prop1].value &= ~mask;
                }
            })
        }
        else {
            Object.defineProperty(this, prop, {
                enumerable: true,
                configurable: false,
                get() {
                    return this.tags[prop].value;
                },
                set(v: any) {
                    this.tags[prop].value = v;
                }
            })
        }
    }
    constructor(tags: T, autoOpen: boolean = true) {
        window.plc = this;
        let cnt = 0;
        for (let k in tags) {
            if (!tags[k].addr) {
                for (let t in tags[k].tags) {
                    const tag = new PLCTag(this, t, tags[k].tags[t][1]);
                    this.tags[t] = tag;
                    this.defineProperty(t);
                    if (typeof tag["mvalue"] == "number") {
                        for (let i = 0; i < 32; i++) {
                            this.defineProperty(t + "_bit" + i);
                        }
                    }
                    if (tags[k].tags[t].length > 2) {
                        tag["mprecision"] = tags[k].tags[t][2];
                        this.defineProperty(t + "_Precision")
                    }
                }
                setTimeout(() => {
                    for (let tagname in tags[k].tags) {
                        this.tags[tagname].active = true;
                        this.tags[tagname]["fire"]("change");
                    }
                }, 1);
            } else {
                cnt++;
                this.mequipments[k] = new Equipment(this, tags[k], k, autoOpen);
            }
        }
        if (!cnt) {
            document.body.style.visibility = "visible";
        }
        AppHandler.mutationsCollector.attachReciver(this);
        AppHandler.mutationsCollector.hotPoints.forEach(hotPoint => {
            this.registrationHotpoint(hotPoint);
        })
    }
    public on<K extends PLCEventType<T>, SCOPE>(type: K,
        callback: K extends TagChangeEventType<T> ? TFEvent<T, this, SCOPE> | FTEvent<T, this, SCOPE> :
            (this: unknown extends SCOPE ? this : SCOPE, e: K extends keyof PLCEventMap<this> ? PLCEventMap<this>[K] : TagChangeEventParam<T>) => void,
        once: boolean = false, scope?: SCOPE
    ) {
        scope = scope || this as unknown as SCOPE;
        switch (typeof type) {
            case "function":
                {
                    const s = type.toString();
                    const otags = s.match(/([a-zA-Z_$][\w$]*?)\.([a-zA-Z_$][\w$]*)/g) || [];
                    const tags = new Set<PLCTag<T>>();
                    (callback as unknown as TagChangeState<T>).active = true;
                    (callback as unknown as TagChangeState<T>).expression = type as any;
                    (callback as unknown as TagChangeState<T>).tags = tags;
                    otags.forEach(e => {
                        const ltag = e.split(/[\._]/)[1];
                        const tag = this.tags[ltag];
                        if (tag) {
                            tags.add(tag);
                        }
                    });
                    for (let i of tags) {
                        i["on"]("change", callback, scope, once);
                    }
                    if (tags.size == 0) throw new Error(`There are no PLC_Tag to refer to. Error code at : on(${s},...)`)
                }
                break;

            case "string":
                {
                    const tag = this.tags[type];
                    if (tag) {
                        tag["on"]("change", callback, scope, once)
                    } else {
                        if (!this.mevents[type]) this.mevents[type] = [];
                        this.mevents[type].push({ callback, scope, once });
                    }
                }
                break;
            case "object":
                if (Array.isArray(type)) {
                    type.forEach(e => {
                        const tag = this.tags[e as string];
                        tag['on']("change", callback, scope, once)
                    })
                    break;
                } else {
                    throw new Error("Unknown event type.")
                }
            default:
                throw new Error("Unknown event type.")
        }
    }

    private fire<K extends keyof PLCEventMap<T>>(type: K, e?: PLCEventMap<this>[K]) {
        const handler = this.mevents[type];
        if (handler) {
            if (!e) e = {} as PLCEventMap<this>[K];
            e.type = type;
            e.sender = this;
            handler.forEach((i, idx, arr) => {
                if (i.once) arr.splice(idx, 1);
                i.callback.call(i.scope, e);
            })
        }
    }

    private registrationHotpoint(hotPoint: HotPoint) {
        const { node, attributeName1, oldValue, notifier: links } = hotPoint;
        const mc1 = oldValue.match(AppHandler.mutationsCollector["regProperty"])
        if (!mc1) return;

        const checkeds = ["checkbox", "radio"];
        const values = ["email", "color", "date", "datetime-local", "email", "password", "month", "tel", "text", "url", "search", "time", "week"]
        const valueasnumber = ["number", "range"];
        const valuetg = ["SELECT", "TEXTAREA"];
        mc1.forEach(str => {
            const mc2 = str.match(/\b[a-zA-Z_]\w*?\b/g);
            if (!mc2) return;
            let tag: PLCTag<any> | undefined;
            mc2.forEach(tagName => {
                tagName = tagName.split("_")[0];
                if (this.tags.hasOwnProperty(tagName)) { tag = this.tags[tagName]; };
                if (!tag) return;
                if (links.has(tag)) return;
                hotPoint.activate = hotPoint.activate && tag["mactive"];
                links.add(tag);
                tag["mhotPoints"].push(hotPoint);
            })
            if (hotPoint.activate && tag) tag["notify"]();
        })
        if (links.size == 0) return;
        if (node.tagName == "INPUT") {
            if (checkeds.includes(node.type) && attributeName1 == "checked") node.addEventListener("change", (e: any) => {
                for (let tag of hotPoint.notifier) tag.value = (<HTMLInputElement>e.target!).checked;
            });
            if (values.includes(node.type) && attributeName1 == "value") node.addEventListener("change", (e: any) => {
                for (let tag of hotPoint.notifier)
                    tag.value = (<Node>e.target).value;
            });
            if (valueasnumber.includes(node.type) && attributeName1 == "value") node.addEventListener("change", (e: any) => {
                for (let tag of hotPoint.notifier)
                    tag.value = (<Node>e.target).valueAsNumber;
            });
        }
        if (valuetg.includes(node.tagName)) node.addEventListener("change", (e: any) => {
            for (let tag of hotPoint.notifier)
                tag.value = (<Node>e.target).value;
        });
    }
}

interface HotPoint {
    node: Node;
    attributeName1: string;
    attributeName2?: string;
    oldValue: string;
    notifier: Set<PLCTag<any>>;
    activate: boolean;
}

class MutationCollector {
    //private regStyle = /([A-Za-z_][\w-]*?) *?:(.*?@\(.+?\)@.*?)(?:(?:;)|(?:$))/g;
    private regStyle = /([A-Za-z_][\w-]*?) ?: ?([^;]*@\(.+?\)@.*?)(?:(?:;)|(?:$))/g;
    private regStyleName = /-[a-z]/g;
    private regProperty = /@\(.+?\)@/g;
    private regFullProperty = /^@\((?:(?!(?:@\()|(?:\)@)).)+\)@$/;

    private mreciver: AppHandler<any> | null = null;
    private mhotPoints: HotPoint[] = [];
    private mobserver: MutationObserver;
    private mobserverOption: MutationObserverInit;
    private oldRecords?: MutationRecord[];
    get hotPoints(): HotPoint[] {
        return this.mhotPoints;
    }
    private removeHotPoint(node: Node, attributeName1?: string, attributeName2?: string) {
        this.mhotPoints.forEach((e, idx, arr) => {
            if (node !== e.node) return;
            if (!attributeName1 || (e.attributeName1 === attributeName1 && e.attributeName2 === attributeName2)) {
                e.notifier.forEach(notifier => {
                    notifier.removeHotPoint(e);
                })
                arr.splice(idx, 1);
            }
        })
    }
    private addHotPoint(hotPoint: HotPoint) {
        this.mhotPoints.push(hotPoint);
        if (this.mreciver) {
            this.mreciver['registrationHotpoint'](hotPoint);
        }
    }
    attachReciver(reciver: AppHandler<any>) {
        this.mreciver = reciver;
    }
    private praseProperty(node: Node, attributeName: string) {
        let newValue;
        if (node.getAttribute) {
            newValue = node.getAttribute(attributeName);
        } else {
            newValue = node[attributeName];
        }
        if (attributeName === "class") attributeName = "className";
        if (!newValue) return;
        switch (attributeName) {
            case "style":
                {
                    let match;
                    this.regStyle.lastIndex = 0;
                    while (match = this.regStyle.exec(newValue)) {
                        const hotPoint: HotPoint = {
                            node: node,
                            attributeName1: "style",
                            attributeName2: match[1].toLowerCase().replace(this.regStyleName, e => e[1].toUpperCase()),
                            oldValue: match[2],
                            activate: true,
                            notifier: new Set()
                        };
                        this.removeHotPoint(node, "style", hotPoint.attributeName2);
                        this.addHotPoint(hotPoint);
                    }
                }
                break;
            default:
                {
                    if (this.regProperty.test(newValue)) {
                        const hotPoint: HotPoint = {
                            node: node,
                            attributeName1: attributeName,
                            attributeName2: undefined,
                            notifier: new Set(),
                            activate: true,
                            oldValue: newValue
                        }
                        this.removeHotPoint(node, attributeName);
                        this.addHotPoint(hotPoint);
                    } else {
                        this.removeHotPoint(node, attributeName);
                    }
                }
                break;
        }
    }
    public start() {
        this.oldRecords && this.documentChange(this.oldRecords);
        this.mobserver.observe(document, this.mobserverOption);
    }
    public stop() {
        this.oldRecords = this.mobserver.takeRecords()
        this.mobserver.disconnect();
    }
    private processedNodes: Node[] = [];
    private timer: number | undefined;
    private recursiveAddNode(node: Node) {
        if (!this.timer) this.timer = setTimeout(() => {
            this.processedNodes = [];
            clearTimeout(this.timer);
            this.timer = undefined;
        }, 0);
        for (let i = node.childNodes.length - 1; i >= 0; i--) {
            this.recursiveAddNode(node.childNodes[i]);
        }
        if (node.getAttributeNames) {
            node.getAttributeNames().forEach((attr: string) => {
                this.praseProperty(node, attr);
            })
        }
        if (node.nodeName === "#text") {
            this.praseProperty(node, "textContent");
        }
        this.processedNodes.push(node);
    }
    private recursiveRemoveNode(node: Node) {
        if (!this.timer) this.timer = setTimeout(() => {
            this.processedNodes = [];
            clearTimeout(this.timer);
            this.timer = undefined;
        }, 0);
        for (let i = node.childNodes.length - 1; i >= 0; i--) {
            this.recursiveRemoveNode(node.childNodes[i]);
        }
        this.removeHotPoint(node);
        this.processedNodes.push(node);
    }
    private documentChange(mutations: MutationRecord[]) {
        for (let mutation of mutations) {
            switch (mutation.type) {
                case "attributes":
                    this.praseProperty(mutation.target, mutation.attributeName!);
                    break;
                case "characterData":
                    this.praseProperty(mutation.target, "textContent");
                    break;
                case "childList":
                    mutation.addedNodes.forEach(el => {
                        if (el === this.processedNodes[this.processedNodes.length - 1]) {
                            this.processedNodes.pop();
                            return;
                        }
                        this.recursiveAddNode(el);
                        this.processedNodes.pop();
                    })
                    mutation.removedNodes.forEach(e => {
                        this.recursiveRemoveNode(e);
                    })
            }
        }
    };
    constructor() {
        this.mobserverOption = {
            attributes: true,
            attributeOldValue: false,
            characterData: true,
            characterDataOldValue: false,
            childList: true,
            subtree: true
        }
        if (document.body) {
            this.recursiveAddNode(document.body);
        }
        this.mobserver = new MutationObserver(this.documentChange.bind(this));
        this.start();
    }

}
AppHandler.mutationsCollector = new MutationCollector();

// 第一种方案
// Window.app 这个变量用于计算HTML上自动显示的内容。
// function Plc<T extends TagsDefine>(tags: T, autoOpen: boolean = true): Tags<T> & AppHandler<T> {
//     window.plc = new AppHandler(tags, autoOpen) as Tags<T> & AppHandler<T>;
//     return window.plc;
// }
// 第二种方案 使用构造签名
type Plc = new <T extends TagsDefine>(tags: T, autoOpen?: boolean) => Tags<T> & AppHandler<T>;
let Plc = AppHandler as unknown as Plc;
declare type PLC<T extends TagsDefine> = Tags<T> & AppHandler<T>;
export {
    DT,
    Plc,
    PLC
}
